[Amazon Connect] 電話アンケートの仕組みを作ってみた 〜 S3に質問内容を置くだけで自動で集計してグラフ表示するのです〜
1 はじめに
AIソリューション部の平内(SIN)です。
今回は、Amazon Connect(以下、Connect)を使用して、電話でアンケートを取る仕組みを作成してみました。
全部、作り込めば、結構簡単にできてしまいそうなので、少しでも汎用的に使えるように、次のような、質問内容を記述したJSONファイルをS3に配置するだけで、自動的に集計(グラフ表示)までするようにしてみました。
surveys.json
{ "welcome_message": "クラスメソッドのアンケートシステムです。", "goodby_message":"ご協力ありがとうございました。今後とも、クラスメソッドをどうぞ宜しくお願い申し上げます。", "surveys":[ { "question":"セミナーの満足度について教えてください。最も良い場合は5、最も悪い場合1の5段階でお応えください。", "list":["最も悪い","悪い","普通","良い","最も良い"] }, { "question":"今回、このセミナーが開催されることを何から知りましたか。ホームページの場合は1を、新聞、雑誌の場合は2を、メール案内による場合は3を、その他の場合は4を押してください。", "list":["ホームページ","新聞、雑誌","メール案内","その他"] }, { "question":"お客様の期待されるセミナーの内容についてお答えください。クラウドサービスは1を、機械学習は2を、モバイルアプリは3を、その他の場合は4を押してください。", "list":["クラウドサービス","機械学習","モバイルアプリ","その他"] } ] }
使用しているようすです。アンケートが終了した時点で、自動的に集計されグラフが更新されます。
2 構成
構成は、次のようになっています。
- アンケートを行うコンタクトフローは、アンケート情報ファイル(surveys.json)を元に動作します。
- アンケート終了時に、回答(answer_XXXXXXXXX)をS3にアップロードします。
- 回答がアップロードされたタイミングで、全ての回答を集めて集計し、集計ファイル(result.json)を生成します。併せて、AWSIoTのMQTTで、グラフ表示をリロードさせます。
- 管理者は、S3上のグラフ表示(index.html)を開いておくだけで、逐次、最新の集計を確認できます。
3 コンタクトフロー
使用したコンタクトフローです。
- 日本語表示設定の後、Lambdaを呼び出します。
- Lambdaの戻り値で、アンケートが終了かどうかを確認します。
- アンケート継続の場合、Lambdaで生成されたメッセージでユーザーの番号入力を待ちます。
- 番号が入力されたら、再び、Lambdaを呼び出します。
- Lambdaの戻り値がアンケート終了の場合は、Lambdaで生成されたメッセージを再生して切断します。
- 2回目以降のLambdaは実行では、ユーザーの番号入力も処理しています。
4 セッション情報について
今回の仕組みでは、アンケート中、質問の数だけLambdaが呼び出されますが、その間、ユーザーの回答を保持する必要があります。
しかし、ConnectのLambda実行では、セッション情報という概念が無いため、保持する情報を文字列化してexport、importするクラスを作成しました。
SessionAttribute.js
// セッション情報を管理するクラス module.exports = class SessionAttributes { constructor(){ this._data = {}; this._data.answer = []; } import(data) { if (data) { this._data = JSON.parse(data); } } export(){ return JSON.stringify(this._data); } get index() { return this._data.answer.length; } get answer() { return this._data.answer; } appendAnswer(num) { this._data.answer.push(num); } }
このクラスを使用して、Lambdaの終了時にexportし、開始時にimportしています。
起動時
const sa = new SessionAttributes(); sa.import(event.Details.Parameters.sa);
終了時
return { sa: sa.export() };
コンタクトフローのLambda呼び出しでは、下記のように、最後のLambdaの戻り値を、パラメータとして読み込んでいます。
この仕組みでは、コンタクトフロー上で、文字列としてデータが保持されるため、どのような型でもセッション情報として使用することが可能です。
なお、外部のデータは、Lambda実行のたびに上書きされますので、別のLambda関数を間に挟むことはできません。
5 リポジトリ
各種のファイルは、S3をリポジトリとして使用して保持していますが、その入出力にために、専用クラスを用意しました。
Repository.js
module.exports = class Repository { constructor(){ const aws_sdk = require('aws-sdk'); this._s3 = new aws_sdk.S3(); this._bucket = 'blog-surveys' this._surveys = 'surveys.json'; // アンケート情報 this._result = 'result.json'; // 集計結果 } async getSurveys() { const data = await this._get(this._surveys); return JSON.parse(data.Body.toString()); } async getAnswer() { let keys = []; const data = await this._list(); data.Contents.forEach( content => { const key = content.Key; if(key.indexOf('answer_') == 0) { keys.push(key); } }); let result = []; for (var i=0; i < keys.length; i++) { const data = await this._get(keys[i]); var obj = JSON.parse(data.Body); result.push(obj.answer); } return result; } async putAnswer(phoneNumber, answer){ const dateStr = (new Date()).toString(); const result = { date: dateStr, phoneNumber: phoneNumber, answer: answer } await this._put('answer_' + dateStr, JSON.stringify(result)) } async putResult(results){ await this._put(this._result, JSON.stringify(results), 'public-read') } async _list() { var params = { Bucket: this._bucket, }; return await this._s3.listObjects(params).promise(); } async _get(key) { var params = { Bucket: this._bucket, Key: key }; return await this._s3.getObject(params).promise(); } async _put(key, body, acl) { var params = { Bucket: this._bucket, Key: key, Body: body, ContentType: 'application/json', ACL: acl }; await this._s3.putObject(params).promise(); } }
S3に各種のファイルが格納されている様子です。
6 Lambda(コンタクトフローから呼ばれる)
コンタクトフローから呼び出されるLambdaのコードは、以下のとおりです。アンケート情報に基づいて、再生するメッセージを返したり、ユーザーの入力を保存しています。
'use strict'; const Repository = require('./Repository.js'); const SessionAttributes= require('./SessionAttributes.js'); exports.handler = async function(event, _context) { console.log(JSON.stringify(event)); if (event.Details) { // コンタクトフローから起動された return await surveyProcessing(event); ・・・省略・・・ async function surveyProcessing(event) { const repository = new Repository(); // S3操作 const sa = new SessionAttributes(); // セッション情報 const json = await repository.getSurveys(); // アンケート取得 let disconnect = false; // 終了フラグ let message = ''; if (event.Details.Parameters.sa == '') { // 初回起動 message += json.welcome_message; // 最初のメッセージ } else { // 2回目以降の起動 sa.import(event.Details.Parameters.sa); // セッション情報の復元 // 入力値の取得 let inputData = -1; try{ //Parameters.inputDataは、'Timeout'の可能性もある const number = Number(event.Details.Parameters.inputData); if (1 <= number && number <= json.surveys[sa.index].list.length) { inputData = number; } } catch(err){ } if(inputData == -1){ message += '入力された番号が無効です。もう一度、お伺いします。' } else { // 回答の保存 sa.appendAnswer(Number(inputData)); } } if(sa.index >= json.surveys.length) { // 終了メッセージ disconnect = true; message = json.goodby_message; // 回答者の電話番号 const phoneNumber = event.Details.ContactData.CustomerEndpoint.Address // アンケート結果の保存 await repository.putAnswer(phoneNumber, sa.answer); } else { // アンケート message += json.surveys[sa.index].question; } return { sa: sa.export(), // セッション情報を文字列としてコンタクトフローに保存する message: message, // 再生されるメッセージ disconnect:disconnect // アンケートを終了して切断するかどうかのフラグ }; }
上記のコードにより、アンケート終了時に生成される回答は、以下のようになっています。
answer_XXXXX
{ "date": "Sat Jan 05 2019 22:55:36 GMT+0000 (UTC)", "phoneNumber": "+8190xxxxxxxx", "answer": [ 5, 2, 1 ] }
7 Lambda(S3への回答保存時に呼ばれる)
回答が保存されたタイミングで呼び出されるLambdaのコードは、以下のとおりです。既に存在する回答を全て取得して、集計ファイルを生成しています。 また、AWSIoTのエンドポイントにPublishして、同トピックをsubscribeしているグラフ表示ブラウザを更新しています。
'use strict'; const Repository = require('./Repository.js'); const Mqtt = require('./Mqtt.js'); const SessionAttributes= require('./SessionAttributes.js'); exports.handler = async function(event, _context) { console.log(JSON.stringify(event)); ・・・省略・・・ } if(event.Records) { // イベントは、サフィックス指定されていますが、念のためフィルタしています。 const key = event.Records[0].s3.object.key; if(key.indexOf('answer_') == 0) { // S3への「answer_」で始まるオブジェクトが、PUTされた return await createView(); } } } async function createView(){ // S3操作 const repository = new Repository(); // アンケート取得 const json = await repository.getSurveys(); // 集計用変数の初期化 const q = Array(json.surveys.length); json.surveys.forEach( (survey,i) => { q[i] = Array.apply(null, Array(survey.list.length)).map( () => {return 0}); }) // 結果取得 const answer = await repository.getAnswer(); // 集計 answer.forEach(a => { q.forEach( (n,i) => { n[a[i] - 1]++; }) }) let results = []; json.surveys.forEach((survey,i) => { results.push({ question: survey.question, list: survey.list, answer: q[i] }); }); await repository.putResult(results); // MQTTでブラウザに更新されたことを伝える const endpoint = 'xxxxxxxxxxxxx.iot.ap-northeast-1.amazonaws.com'; const topic = "surveys_refresh"; const mqtt = new Mqtt(); await mqtt.refresh(endpoint, topic); }
結果として出力される集計ファイル(result.json)は、以下のようになっています。グラフ表示(index.html)は、このJSONを元に表示されています。
result.json
[ { "question": "セミナーの満足度について教えてください。最も良い場合は5、最も悪い場合1の5段階でお応えください。", "list": [ "最も悪い", "悪い", "普通", "良い", "最も良い" ], "answer": [ 2, 1, 1, 1, 13 ] }, { "question": "今回、このセミナーが開催されることを何から知りましたか。ホームページの場合は1を、新聞、雑誌の場合は2を、メール案内による場合は3を、その他の場合は4を押してください。", "list": [ "ホームページ", "新聞、雑誌", "メール案内", "その他" ], "answer": [ 1, 10, 4, 3 ] }, { "question": "お客様の期待されるセミナーの内容についてお答えください。クラウドサービスは1を、機械学習は2を、モバイルアプリは3を、その他の場合は4を押してください。", "list": [ "クラウドサービス", "機械学習", "モバイルアプリ", "その他" ], "answer": [ 14, 1, 2, 1 ] } ]
8 最後に
今回は、汎用的に利用できる電話によるアンケートの仕組みを作ってみました。
同じアンケート情報(設定ファイル)で、Web回答できるページも生成するようにすれば、2種類のUIを提供できるシステムとなって、より汎用的かも知れません。
多様なUIを提供する方法の1つとして、Connectは、有効だと感じています。
グラフの生成については、下記のものを利用させて頂きました。
Google Chats -Visualization: Pie Chart-
弊社では、「Amazon Connect」の導入を検討している方を対象とした無料相談会を毎週開催中です。
また、音声を利用した各種ソリューションの導入支援を行っております。お気軽にお問い合わせください。